/**
 * 
 */
package br.com.listatel.modelo.frameworkDao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import br.com.listatel.modelo.frameworkDao.QueryMapper;

/**
 * 
 * @author danilo
 */
public class EngineConnection {
	Connection conn = null;

	private static final ThreadLocal<Connection> context = new ThreadLocal<Connection>();

	/**
	 * Este método tem a função de retornar a conexão de maneira simples, sem a
	 * intervenção da thread local
	 * 
	 * @return
	 * @throws SQLException
	 */
	public Connection getConnection() throws SQLException {
		try {
			Class.forName("com.mysql.jdbc.Driver");
			return DriverManager.getConnection(
					"jdbc:mysql://localhost/listatel", "root", "apolo21");
		} catch (ClassNotFoundException e) {
			throw new SQLException("Impossivel abrir conexão", e);
		}
	}

	/**
	 * Este método tem a função de criar uma transação e vincular esta transação
	 * na thread local, dando a possibilidade de trabalhar com pool de
	 * transações
	 * 
	 * @throws SQLException
	 */
	public void beginTransaction() throws SQLException {
		if (conexaoStarted())
			return;
		Connection conn = getConnection();
		conn.setAutoCommit(false);
		context.set(conn);
	}

	/**
	 * Salva as alterações no banco e finaliza a transação
	 * 
	 * @throws SQLException
	 */
	public void endTransaction() throws SQLException {
		commit(getConnectionFromContext());
		releaseTransaction();
	}

	/**
	 * Finaliza a conexão gerada pelo contexto e remove esta da thread local
	 * (contexto)
	 * 
	 * @throws SQLException
	 */
	public void releaseTransaction() throws SQLException {
		Connection conn = getConnectionFromContext();
		release(conn);
		context.remove();
	}

	/**
	 * Desfaz a transação incompleta, fecha a conexão e remove esta do contexto.
	 * 
	 */
	public void rollbackTransaction() {
		Connection conn;
		try {
			conn = getConnectionFromContext();
			rollback(conn);
			release(conn);
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		context.remove();
	}

	private void commit(Connection conn) throws SQLException {
		conn.commit();
	}

	private void rollback(Connection conn) throws SQLException {
		if (conn != null)
			conn.rollback();
	}

	/**
	 * Este método tem a função de verificar se a thread local contém a ausência
	 * de conexões startadas
	 * 
	 * @return
	 */
	public boolean conexaoStarted() {
		return (context.get() != null);
	}

	/**
	 * Este método tem a função de retornar a conexão criada na thread local
	 * 
	 * @return
	 * @throws SQLException
	 */
	public Connection getConnectionFromContext() throws SQLException {

		Connection conn = context.get();

		if (conn == null)
			throw new SQLException("Transação invalida");

		if (conn.isClosed())
			throw new SQLException(
					"Transação invalida, Conexao esta fechada :Z");

		return conn;
	}

	/**
	 * Este método tem a função de realizar o update no banco de dados, além de
	 * retornar o id gerado pelo banco, armazenando este no próprio objeto
	 * 
	 * @param conn
	 * @param query
	 * @param params
	 * @return
	 * @throws SQLException
	 */
	public long executePreparedUpdateAndReturnGeneratedKeys(Connection conn,
			String query, Object... params) throws SQLException {

		PreparedStatement pstmt = null;
		ResultSet rset = null;
		long result = 0l;
		try {
			pstmt = conn.prepareStatement(query,
					PreparedStatement.RETURN_GENERATED_KEYS);
			int i = 0;
			for (Object param : params) {
				pstmt.setObject(++i, param);
			}

			pstmt.executeUpdate();

			rset = pstmt.getGeneratedKeys();

			if (rset.next())
				result = rset.getLong(1);

		} finally {
			release(pstmt);
			release(rset);
		}
		return result;
	}

	/**
	 * Este método tem a função de realizar o update no banco de dados, além de
	 * retornar o id gerado pelo banco, armazenando este no próprio objeto.
	 * Nesta variação, não é necessário passar a conexão como parâmetro.
	 * 
	 * @param query
	 * @param params
	 * @throws SQLException
	 */
	public long executePreparedUpdateAndReturnGeneratedKeys(String query,
			Object... params) throws SQLException {

		long id = executePreparedUpdateAndReturnGeneratedKeys(
				getConnectionFromContext(), query, params);
		return id;
	}

	/**
	 * Este método apenas armazena os dados no banco, sem o retorno do ID gerado
	 * pelo mesmo
	 * 
	 * @param conn
	 * @param query
	 * @param params
	 * @throws SQLException
	 */
	public void executePreparedUpdate(Connection conn, String query,
			Object... params) throws SQLException {
		PreparedStatement pstmt = null;
		try {
			pstmt = conn.prepareStatement(query);
			int i = 0;
			for (Object param : params) {
				pstmt.setObject(++i, param);
			}
			pstmt.executeUpdate();
		} finally {
			release(pstmt);
		}
	}

	/**
	 * Este método armazena os dados no banco sem a obrigatoriedade de passar
	 * como parâmetro a conexão, pois esta (conexão simples) é gerada pelo
	 * próprio método. Assim como o simples update, este também não retorna o ID
	 * 
	 * @param query
	 * @param params
	 * @throws SQLException
	 */
	public void executePreparedUpdateIntoSingleTransaction(String query,
			Object... params) throws SQLException {
		executePreparedUpdate(getConnection(), query, params);
	}

	/**
	 * Este método armazena os dados no banco sem a obrigatoriedade de passar
	 * como parâmetro a conexão, pois esta (conexão pelo contexto) é gerada pelo
	 * próprio método. Assim como o simples update, este também não retorna o ID
	 * 
	 * @param query
	 * @param params
	 * @throws SQLException
	 */
	public void executePreparedUpdateIntoContextTransaction(String query,
			Object... params) throws SQLException {

		executePreparedUpdate(getConnectionFromContext(), query, params);
	}

	/**
	 * Este método retorna uma lista de todos os dados presentes no banco de dados
	 * 
	 * @param query
	 * @param mapper
	 * @return
	 * @throws SQLException
	 */
	//Entender como esta merda funciona é fundamentel! KK
	public <T> List<T> executePreparedQuery(String query, QueryMapper<T> mapper)
			throws SQLException {

		Connection conn = null;
		Statement stmt = null;
		ResultSet rset = null;

		List<T> list = new ArrayList<T>();

		try {
			conn = getConnection();
			stmt = conn.createStatement();
			rset = stmt.executeQuery(query);
			list = mapper.mapping(rset);
		} finally {
			releaseAll(conn, stmt, rset);
		}

		return list;

	}

	/**
	 * Fecha Statement
	 * 
	 * @param stmt
	 */
	public void release(Statement stmt) {
		if (stmt == null)
			return;
		try {
			stmt.close();
		} catch (SQLException e) {
		}
	}

	/**
	 * Fecha a conexão
	 * 
	 * @param conn
	 */
	public void release(Connection conn) {

		if (conn == null)
			return;
		try {
			conn.close();
		} catch (SQLException e) {
		}

	}

	/**
	 * Fecha ResultSet
	 * 
	 * @param rset
	 */
	public void release(ResultSet rset) {
		if (rset == null)
			return;
		try {
			rset.close();
		} catch (SQLException e) {
		}
	}

	/**
	 * Fecha a conexão e Statement
	 * 
	 * @param conn
	 * @param stmt
	 */
	public void releaseAll(Connection conn, Statement stmt) {
		release(stmt);
		release(conn);
	}

	/**
	 * Fecha a conexão, Statement e ResultSet
	 * 
	 * @param conn
	 * @param stmt
	 * @param rset
	 */
	public void releaseAll(Connection conn, Statement stmt, ResultSet rset) {
		releaseAll(conn, stmt);
		release(rset);
	}
}
